const cache = new Map();

function json_parser(k, v) {
  // 识别ISO8601格式UTC时间，转换为Date类型
  if (typeof v === 'string' && v.length >= 19 && v.length <= 24) {
    if (/^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2}(?:\.\d*)?)(Z)?$/.test(v))
      return new Date(v);
  }
  return v;
}

const A = {
  /**
   * 判断是否存在缓存项
   * @param {string} key 键名称
   * @returns 存在缓存项时返回true, 否则返回false
   */
  has: (key) => cache.has(key),

  /**
   * 获取缓存内容
   * @param {string} key 键名称
   * @returns 返回缓存的内容
   */
  get: (key) => cache.get(key),

  /**
   * 设置缓存内容, 如果值为undefined, 则删除缓存项
   * @param {string} key 键名称
   * @param {any} value 值
   * @returns returns of delete() or set()
   */
  set: (key, value) => value === undefined ? cache.delete(key) : cache.set(key, value),

  /**
   * 设置token
   * @param {string} token token
   */
  setToken: token => A.set('TOKEN', token),

  /**
   * 清除token
   */
  clearToken: () => A.set('TOKEN', undefined),

  /**
   * 二次封装的fetch版本, 请求携带cookie和token
   * @param {string} url URL
   * @param {object} init 同window.fetch的init参数
   * @param {bool || function} json 是否将响应解析为JSON, 指定为函数可以替代默认的Parser
   * @returns Promise<any>
   */
  fetch: async function (url, init) {
    return await fetch(url, {
      credentials: 'include',
      ...init,
      headers: Object.assign({}, A.has('TOKEN') ? { 'Authorization': 'Bearer ' + A.get('TOKEN') } : null, init?.headers),
    });
  },

  /**
   * 封装文本输出的fetch方法. 自动识别错误信息
   * @param {string} url URL
   * @param {object} init 同window.fetch的init参数
   * @returns 
   */
  fetchText: async function (url, init) {
    return await A.fetch(url, init).then(async (response) => {
      if (response.ok) {
        return response.text();
      }
      let text = await response.text();
      if (response.status === 404) {
        throw Error(response.status + ' ' + response.statusText + ": " + url);
      } else {
        if (text) throw Error(text)
        else throw Error(response.status + ' ' + response.statusText);
      }
    });
  },

  /**
   * 封装JSON输出的fetch方法. 响应进行JSON反序列化
   * @param {string} url URL
   * @param {object} init 同window.fetch的init参数
   * @param {function} parser JSON解析函数
   * @returns 
   */
  fetchJson: async function (url, init, parser) {
    return await A.fetchText(url, init).then((text) => {
      return JSON.parse(text, parser ?? json_parser);
    });
  },

  /**
   * 调用服务端作业
   * @param {string} name 作业名称
   * @param {any} param 作业参数
   * @param {function} parser 可选的JSON解析函数
   * @returns Promise<any>
   */
  job: async function (name, param, parser) {
    return await A.fetchJson("/api/Job", {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({
        Name: name,
        Param: param,
      })
    }, parser);
  },

  /**
   * 文档访问接口集
   */
  doc: {
    /**
     * 构造文件访问链接
     * @param {string} file 要访问的文件路径
     * @returns 链接URL
     */
    href(file) {
      return `/api/doc?access_token=${A.get('TOKEN')}&file=${encodeURIComponent(file)}`;
    },

    /**
     * 下载文件到本地
     * @param {string} file 要下载的文件路径
     * @param {string} rename 本地保存文件名
     */
    download(file, rename) {
      if (!rename) rename = file.split('/').pop();
      const a = document.createElement('a');
      a.href = `/api/doc?file=${encodeURIComponent(file)}&name=${encodeURIComponent(rename)}&access_token=${A.get('TOKEN')}`;
      a.download = rename;
      a.click();
    },

    /**
     * 选择文件上传. 可支持多选
     * @param {string} dir 文件保存位置
     * @param {object} attr 设置给input的属性
     * @param {files => true} validator 校验回调函数, 传入选择的文件数组, 校验不通过时返回false
     * @returns Promise(paths), paths是文件保存路径的数组, 与files顺序相同
     */
    upload_files(dir, attr = {}, validator = files => true) {
      return new Promise((resolve, reject) => {
        // 弹窗选择文件
        const input = document.createElement('input');
        Object.keys(attr).forEach(key => input.setAttribute(key, attr[key]));
        input.setAttribute('type', 'file');
        input.onchange = () => {
          const files = Array.from(input.files);
          if (files.length === 0) return reject();
          if (false === validator(files)) return reject();

          const form = new FormData();
          files.forEach(file => form.append('files', file, file.name));
          form.append('dir', dir);
          A.fetchJson("/api/doc", {
            method: 'POST',
            body: form
          }).then(paths => resolve(paths)).catch(e => reject(e));
        };
        input.click();
      });
    },

    /**
     * 上传Base64编码的文件数据
     * @param {string} data base64编码后的文本
     * @param {string} ext 文件名的扩展名部分
     * @param {string} dir 文件保存位置
     * @returns Promise<path>, path是文件保存路径
     */
    async upload_base64(data, ext, dir) {
      const form = new FormData();
      form.append('data', data);
      form.append('ext', ext);
      form.append('dir', dir);
      return await A.fetchJson("/api/doc/base64", {
        method: 'POST',
        body: form
      });
    },

    /**
     * 支持续传的大文件分块上传. 可支持多选
     * @param {string} dir 文件保存位置
     * @param {object} attr 传递给input组件的属性
     * @param {files => true} validator 校验回调函数, 传入选择的文件数组, 校验不通过时返回false
     * @returns Promise(paths), paths是文件保存路径的数组, 与files顺序相同
     */
    upload_blocks(dir, attr = {}, validator = files => true) {
      const chunk_size = 1024 * 1024;
      return new Promise((resolve, reject) => {
        // 弹窗选择文件
        const input = document.createElement('input');
        Object.keys(attr).forEach(key => input.setAttribute(key, attr[key]));
        input.setAttribute('type', 'file');
        input.onchange = () => {
          const files = Array.from(input.files);
          if (files.length === 0) return reject();
          if (false === validator(files)) return reject();

          Promise.all(files.map(file => {
            return new Promise((file_resolve, file_reject) => {
              const form = new FormData();
              form.append('name', file.name);
              form.append('dir', dir);
              A.fetchJson("/api/doc/begin", {
                method: 'POST',
                body: form,
              }).then(exists => {
                const chunk_count = Math.ceil(file.size / chunk_size);
                Promise.all(Array.from({ length: chunk_count }).map((v, i) => {
                  var chunk = file.slice(i * chunk_size, (i + 1) * chunk_size);
                  // 如果块已经传输过了, 就跳过
                  if (exists.hasOwnProperty(i * chunk_size)) return Promise.resolve();
                  var form = new FormData();
                  form.append("data", chunk);
                  form.append("offset", i * chunk_size);
                  form.append("name", file.name);
                  form.append("dir", dir);
                  return A.fetchText('/api/doc/block', {
                    method: 'POST',
                    body: form,
                  });
                })).then(() => {
                  const form = new FormData();
                  form.append('name', file.name);
                  form.append('dir', dir);
                  A.fetchJson("/api/doc/end", {
                    method: 'POST',
                    body: form
                  }).then(path => file_resolve(path)).catch(e => file_reject(e));
                }).catch(e => file_reject(e));
              }).catch(e => file_reject(e));
            })
          })).then(files => resolve(files)).catch(errors => reject(errors));
        };
        input.click();
      });
    },

    /**
     * 将文件复制到新位置. 会在新位置中创建新的文件名
     * @param {string} file 源文件路径
     * @param {string} dir 目标位置
     * @returns Promise<undefined>
     */
    async copy(file, dir) {
      const form = new FormData();
      form.append('file', file);
      form.append('dir', dir);
      return await A.fetchText("/api/doc/copy", {
        method: 'POST',
        body: form
      });
    },

    /**
     * 删除指定的文件
     * @param {string} file 文件路径
     * @returns Promise<undefined>
     */
    async delete(file) {
      const form = new FormData();
      form.append('file', file);
      return await A.fetchText("/api/doc/delete", {
        method: 'POST',
        body: form
      });
    },
  },

  /**
   * 通过作业实现的Excel数据导入
   * @param {string} name 处理导入数据的作业的名称
   * @param {object} param 处理导入数据的作业的附加参数
   * @param {file => true} validator 校验回调函数, 传入选择的文件, 校验不通过时返回false
   * @returns 作业的返回值
   */
  import: function (name, param, validator = file => true) {
    return new Promise((resolve, reject) => {
      // 弹窗选择文件
      const input = document.createElement('input');
      input.setAttribute('type', 'file');
      input.setAttribute('accept', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet,application/vnd.ms-excel');
      input.onchange = () => {
        const [file] = input.files;
        if (!file) return reject();
        if (false === validator(file)) return reject();

        const form = new FormData();
        form.append('file', file, file.name);
        form.append('name', name);
        if (param != null) form.append('args', JSON.stringify(param));
        A.fetchJson("/api/office/import", {
          method: 'POST',
          body: form
        }).then(result => resolve(result)).catch(e => reject(e));
      };
      input.click();
    });
  },

  /**
   * 基于模板的数据导出接口集
   */
  export: {
    /**
     * 基于Excel模板导出作业执行的结果数据
     * @param {string} name 作业名称
     * @param {object} param 作业参数
     * @param {string} template 模板文件的路径
     * @returns 导出文件的服务器路径
     */
    xls: async function (name, param, template) {
      return await A.fetchJson("/api/office/export?template=" + (template ?? ''), {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
        },
        body: JSON.stringify({
          Name: name,
          Param: param,
        })
      });
    },

    /**
     * 基于PDF模板导出作业执行的结果数据
     * @param {string} name 作业名称
     * @param {object} param 作业参数
     * @param {string} template 模板文件的路径
     * @returns 导出文件的服务器路径
     */
    pdf: async function (name, param, template) {
      return await A.fetchJson("/api/pdf/export?template=" + (template ?? ''), {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
        },
        body: JSON.stringify({
          Name: name,
          Param: param,
        })
      });
    },
  },
};

export default A;